"""
《邢不行-2020新版|Python数字货币量化投资课程》
无需编程基础，助教答疑服务，专属策略网站，一旦加入，永续更新。
课程详细介绍：https://quantclass.cn/crypto/class
邢不行微信: xbx3636
本程序作者: 邢不行

# 课程内容
币安期现套利
"""
import calendar
import time
from datetime import datetime, timedelta, timezone

import numpy as np
import pandas as pd

from Common import retry_wrapper
from Config import *


# ===判断并更改为单项持仓模式
def if_oneway_mode():
    """
    判断当前合约持仓模式。必须得是单向模式。False为单向持仓，True为单向持仓
    """
    # 查询持仓模式(USER_DATA)
    # GET /dapi/v1/positionSide/dual (HMAC SHA256)
    positionSide = retry_wrapper(exchange.dapiPrivateGetPositionSideDual, func_name='查看合约持仓模式')
    if positionSide['dualSidePosition'] is True:
        params = {
            'dualSidePosition': 'false',
            'timestamp': int(time.time() * 1000),
        }
        # 更改持仓模式(TRADE)
        # POST /dapi/v1/positionSide/dual (HMAC SHA256)
        retry_wrapper(exchange.dapiPrivatePostPositionSideDual, params, func_name='更改合约持仓模式')
        print('更改持仓模式为：单向持仓\n')
    else:
        print('当前持仓模式：单向持仓\n')


# ===时间格式转换函数===
def time_strptime_timestamp(time_type, time_para, time_format):
    """
    时间工具函数：
        原文链接：https://community.modelscope.cn/63a01df7dacf622b8df9143e.html
        通用时间函数：
            time_type==1，将标准时间转化为UNIX时间戳(ms级别)
            time_type==2，将UNIX时间戳(ms级别)转化为标准时间
            time_type==3，重新格式化时间
            time_type==4，将标准时间转换成pd.to_datetime(类型为Timestamp)
            time_type==5，将pd.to_datetime(类型为Timestamp)转换UNIX成时间戳(ms级别)

    概念讲解：
        UTC(time Coordinated Universal Time)与timestamp(时间戳)：
        原文链接：https://blog.csdn.net/weixin_41951954/article/details/119761290

    标准时间与时间戳转换在线工具：https://tool.lu/timestamp/
    """

    res = ''
    time_type = int(time_type)
    # time_format = '%Y-%m-%d %H:%M:%S'

    # 将标准时间转化为UNIX时间戳；入参time_type如：'2021-01-01 08:18:28'
    if time_type == 1:
        struct_time = time.strptime(time_para, time_format)
        res = int(time.mktime(struct_time) * 1000)  # 币安所需时间参数一般为long类型，ms级别

    # 将UNIX时间戳转化为标准时间(北京)；入参time_type如：1609430400000(ms级别)
    elif time_type == 2:
        # 转换成localtime
        time_local = time.localtime(int(time_para / 1000))
        # 转换成新的时间格式(2016-05-05 20:28:54)
        res = time.strftime(time_format, time_local)

    # 将pd.to_datetime(类型为Timestamp)转换成UNIX时间戳(ms级别)；入参time_type如：'2021-01-01 08:18:28'
    elif time_type == 3:
        time_para -= timedelta(hours=8)  # 直接转换的话为uat时间，需要换算成北京时间
        res = int(time_para.value / 10 ** 6)

    # 传入datetime格式日期；入参time_para如：datetime(2023, 7, 16, 12, 30, 00)
    elif time_type == 4:
        res = time_para.replace(tzinfo=timezone.utc).astimezone()

    # 将UNIX时间戳转化为标准时间(UTC)；入参time_type如：1609430400000(ms级别)
    elif time_type == 5:
        dt = datetime.fromtimestamp(time_para / 1000)  # 将时间戳转换为datetime对象
        utc_dt = dt.astimezone(timezone.utc)  # 将时间转换为UTC时间
        res = utc_dt.strftime(time_format)

    return res


# 获取每个月最后一个周五的日期，返回格式如：2023-06-30
def get_last_friday(year, month):
    last_day = calendar.monthrange(year, month)[1]
    for day in range(last_day, 0, -1):
        date = datetime.date(year, month, day)
        if date.weekday() == calendar.FRIDAY:
            return date


# 在现货账户下限价单
def binance_spot_place_order(exchange, spot_symbol, long_or_short, price, amount):
    """
    :param exchange:  ccxt交易所
    :param spot_symbol: 现货交易对代码，例如'BTCUSDT'
    :param long_or_short:  两种类型：买入、卖出
    :param price:  下单价格
    :param amount:  下单数量
    :return:
    """

    for i in range(5):
        try:
            params = {
                'symbol': spot_symbol,
                'side': binance_order_type[long_or_short],
                'type': 'LIMIT',
                'timeInForce': 'GTC',
                'quantity': amount,
                'price': price,
                'timestamp': int(time.time() * 1000),
            }
            # 现货下单 (TRADE)
            # POST /api/v3/order (HMAC SHA256)
            order_info = exchange.privatePostOrder(params=params)
            # todo 如何确定成功买入而不是挂单成功
            print(f'现货交易下单成功：', spot_symbol, long_or_short, price, amount)
            print('下单信息：', order_info, '\n')
            return order_info
        except Exception as e:
            print('binance现货交易下单报错，1s后重试', e)
            time.sleep(1)
    print('binance现货交易下单报错次数过多，程序终止')
    exit()


# 在期货合约账户下限价单
def binance_future_place_order(exchange, future_symbol, long_or_short, price, amount):
    """
    :param exchange:  ccxt交易所
    :param future_symbol: 合约代码，例如'BTCUSD_230929'
    :param long_or_short:  四种类型：开多、开空、平多、平空
    :param price: 开仓价格
    :param amount: 开仓数量，这里的amount是合约张数
    :return:
    """
    # 确定下单参数
    # 开空
    params = {
        'symbol': future_symbol,
        'side': binance_order_type[long_or_short],
        'type': 'LIMIT',
        'timeInForce': 'GTC',
        'quantity': amount,
        'price': price,
        'timestamp': int(time.time() * 1000),
    }
    # 尝试下单
    for i in range(5):
        try:
            # 下单 (TRADE)
            # POST /dapi/v1/order (HMAC SHA256)
            order_info = exchange.dapiPrivatePostOrder(params)
            print('币安合约交易下单成功：', future_symbol, long_or_short, price, amount)
            print('下单信息：', order_info, '\n')
            return order_info
        except Exception as e:
            print('币安合约交易下单报错，1s后重试...', e)
            time.sleep(1)

    print('币安合约交易下单报错次数过多，程序终止')
    exit()


# 获取账户中交易对的数量
def fetch_symbol_assets_num(symbol, account):
    # balance = exchange.fetch_balance()
    symbol_balance = 0
    params = {
        'asset': symbol,
        'timestamp': int(time.time() * 1000),
    }
    if account == ('spot' or 'SPOT'):
        # 用户持仓 (USER_DATA)
        # POST /sapi/v3/asset/getUserAsset
        balance = exchange.sapiV3PostAssetGetUserAsset(params=params)
        if balance:
            symbol_balance = balance[0]['free']
    elif account == ('usdt' or 'USDT'):
        # 账户信息 (USER_DATA)
        # GET /fapi/v2/account (HMAC SHA256)
        balance = pd.DataFrame(exchange.fapiPrivateGetAccount({'timestamp': int(time.time() * 1000)})['assets'])
        symbol_balance = balance.loc[balance['asset'] == symbol, 'walletBalance'].iloc[0]  # symbol结尾不带USDT
    elif account == ('coin' or 'COIN'):
        # 账户信息 (USER_DATA)
        # GET /dapi/v1/account (HMAC SHA256)
        balance = pd.DataFrame(exchange.dapiPrivateGetAccount({'timestamp': int(time.time() * 1000)})['assets'])
        symbol_balance = balance.loc[balance['asset'] == symbol, 'walletBalance'].iloc[0]
    return float(symbol_balance)


# todo 获取各账户中交易对的持仓数量
def fetch_symbol_positions_num(symbol, trade_type):
    symbol_postitions = 0
    if trade_type == 'coin' or 'COIN':
        # 账户信息(USER_DATA)
        # GET /dapi/v1/account (HMAC SHA256)
        postitions = pd.DataFrame(exchange.dapiPrivateGetAccount({'timestamp': int(time.time() * 1000)})['positions'])
        symbol_postitions = \
            postitions.loc[postitions['symbol'] == symbol + future_date, 'positionAmt'].iloc[0]  # positionAmt为持仓的合约张数
    return float(symbol_postitions)


# 获取账户合约账户中交易对可转账的数量
# todo maxWithdrawAmount如何计算的
def fetch_symbol_max_withdraw_amount(symbol, trade_type):
    max_withdraw_amount = 0
    if trade_type == 'coin' or 'COIN':
        # 账户信息 (USER_DATA)
        # GET /dapi/v1/account (HMAC SHA256)
        balance = pd.DataFrame(exchange.dapiPrivateGetAccount({'timestamp': int(time.time() * 1000)})['assets'])
        max_withdraw_amount = balance.loc[balance['asset'] == symbol, 'maxWithdrawAmount'].iloc[0]
    return float(max_withdraw_amount)


# binance现货与合约账户转账
def binance_account_transfer(exchange, symbol, amount, from_account, to_account):
    if from_account == '现货' and to_account == '合约':
        transfer_type = 'MAIN_CMFUTURE'
    elif from_account == '合约' and to_account == '现货':
        transfer_type = 'CMFUTURE_MAIN'
    else:
        raise ValueError('未能识别`from_account`和`to_account`的组合，请参考官方文档')

    # 构建参数
    params = {
        'type': transfer_type,
        'asset': symbol,
        'amount': amount,  # 这里amount为币的数量(币本位合约中不是币的张数)，类型为DECIMAL，传值可忽略精度
    }

    # 开始转账
    for i in range(5):
        try:
            params['timestamp'] = int(time.time() * 1000)
            # 用户万向划转 (USER_DATA)
            # POST /sapi/v1/asset/transfer (HMAC SHA256)
            transfer_info = exchange.sapiPostAssetTransfer(params=params)
            print('转账成功：', from_account, 'to', to_account, amount)
            print('转账信息：', transfer_info, '\n')
            return transfer_info
        except Exception as e:
            print('转账报错，1s后重试', e)
            time.sleep(1)

    print('转账报错次数过多，程序终止')
    exit()


# 获取所有交易对
def fetch_current_symbol_list(trade_type):
    exchange_info = []
    # 现货
    if trade_type.lower() == 'spot':
        # 获取现货交易规则和交易对
        # GET /api/v3/exchangeInfo
        exchange_info = exchange.publicGetExchangeInfo()
    # U本位合约
    elif trade_type.lower() == 'usdt':
        # 获取u本位合约交易规则和交易对
        # GET /fapi/v1/exchangeInfo
        exchange_info = exchange.fapiPublicGetExchangeInfo()
    # 币本位合约
    elif trade_type.lower() == 'coin':
        # 获取币本位合约交易规则和交易对
        # GET /dapi/v1/exchangeInfo
        exchange_info = exchange.dapiPublicGetExchangeInfo()
    df = pd.DataFrame(exchange_info['symbols'])
    symbol_list_all = list(df['symbol'])
    np.save(f'data/{trade_type}_current_symbol_list.npy', symbol_list_all)
    print(f'{trade_type.upper()}市场现有交易币对：\n{symbol_list_all}')
